package de.fuberlin.enginetesting;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import de.fuberlin.enginetesting.Algorithms.AlgName;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.BatteryManager;
import android.telephony.TelephonyManager;

public class Engine {

	//Constants
	//Right now we assume we only have 1 server (in the future we could search the nearest server)
	public static final String SERVER_URL = "http://www.mi.fu-berlin.de/offload/run";
	public static final String PREFS_NAME = "OffloadingEnginePrefs";
	public static final long SERVER_INST_MS = 6666666; //Calculated through practical values (javap -c), although we know the server has 4 cores of 2.5GHz

	private Context appContext; //The context of the main Activity of the Android application using this engine
	private int pingCounter; //Needed to calculate the ping
	private long timePingStart; //Needed to calculate the ping
	private long[] pingsArray; //Needed to calculate the ping
	private long ping; //Represents the time to query and get an answer from the server (actually not done with a real ping command over ICMP) 
	private boolean connAvailable;
	private boolean serverAvailable;
	private String connType; //Not needed, just to display info
	private boolean connWiFi; //true = WiFi, false = any other connection type (we will assume 3G is the only other possibility)
	private String batteryStatus; //Not needed, just to display info. Keeps updated during the execution.
	private boolean pluggedDevice; //true if the battery is plugged (both AC and USB), false otherwise. Keeps updated during the execution.
	private double currentBattery; //In %. Keeps updated during the execution.
	
	//The next 7 variables are not needed. Just to display info.
	private String realServerTime; //It will have the time in milliseconds that the server needed to execute an algorithm (in case of offloading)
	private double androidEstimatedRuntime;
	private double offloadingEstimatedTime;
	private double serverEstimatedRuntime;
	private double androidEstimatedNeededEnergy;
	private double offloadingEstimatedNeededEnergy;
	private boolean doOffloading;
	private long overallTime;
	
	//Persistent values, will be loaded (calculated only in the first execution of the Android application using this engine)
	private float relationServerAndroid;
	private float batteryPerInst;
	private float battery3G;	//Estimated battery used for sending packets with 3G connection
	private float batteryWiFi;	//Estimated battery used for sending packets with Wi-Fi connection. Not needed, just to display info.

	public Engine(Context theContext) {
		appContext = theContext;
		//We initialize some variables with default values although they will be properly obtained soon (just in case there is an early potentially offloadable algorithm)
		ping = 100;
		connAvailable = true;
		serverAvailable = true;
		connType = "Unknown";
		connWiFi = true; 
		batteryStatus = "Unknown";
		pluggedDevice = false;
		currentBattery = 50;
		loadEngine();
		keepBatteryUpdated();
		getConnectionDetails();
		if (connAvailable) averagePing();
	}

	//Getters, not needed, just to display info
	public long getPing() {
		return ping;		
	}

	public String getBatteryStatus(){
		return batteryStatus;	
	}

	public float getRelation() {
		return relationServerAndroid;		
	}
	
	public float getBatteryPerInstruction() {
		return batteryPerInst;		
	}
	
	public float getBatteryWiFi() {
		return batteryWiFi;		
	}
	
	public float getBattery3G() {
		return battery3G;		
	}
	
	public String getConnType() {
		return connType;
	}
	
	public boolean isConnectionAvailable() {
		return connAvailable;
	}
	
	public boolean isServerAvailable() {
		return serverAvailable;
	}
	
	public String getRealServerTime() {
		return realServerTime;
	}
	
	public double getCurrentBattery() {
		return currentBattery;
	}
	
	public double getAndroidEstimatedRuntime() {
		return androidEstimatedRuntime;
	}
	
	public double getOffloadingEstimatedTime() {
		return offloadingEstimatedTime;
	}
	
	public double getAndroidEstimatedNeededEnergy() {
		return androidEstimatedNeededEnergy;
	}
	
	public double getOffloadingEstimatedNeededEnergy() {
		return offloadingEstimatedNeededEnergy;
	}
	
	public boolean isOffloadingDone() {
		return doOffloading;
	}
	
	public long getOverallTime() {
		return overallTime;		
	}
	
	public double getServerEstimatedRuntime() {
		return serverEstimatedRuntime;
	}
	//End getters

	private void getConnectionDetails() {
		ConnectivityManager connMgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
		if (networkInfo != null && networkInfo.isConnected()) {
			connAvailable = true;
			int netType = networkInfo.getType();
			int netSubtype = networkInfo.getSubtype();
			if (netType == ConnectivityManager.TYPE_WIFI) {
				connWiFi = true;
				connType = "Wi-Fi";
			}
			else {
				connWiFi = false;
				if(netType == ConnectivityManager.TYPE_MOBILE  && netSubtype == TelephonyManager.NETWORK_TYPE_UMTS) connType = "3G";
				else connType = "Other";
			}	
		}
		else {
			connAvailable=false;
			serverAvailable = false;
		}		
	}

	private void keepBatteryUpdated() {

		BroadcastReceiver batteryStatusReceiver = new BroadcastReceiver() {
			@Override
			public void onReceive(Context context, Intent intent) {
				int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
				if (plugged == BatteryManager.BATTERY_PLUGGED_AC) {
					pluggedDevice = true;
					batteryStatus = "On AC power";
				} else if (plugged == BatteryManager.BATTERY_PLUGGED_USB) {
					pluggedDevice = true;
					batteryStatus = "On USB power";
				} else if (plugged == 0) {
					pluggedDevice = false;
					batteryStatus = "On battery power";
				} else {
					pluggedDevice = false;
					batteryStatus = "Intent didnt include extra info";
				}
			}
		};

		BroadcastReceiver batteryPercentReceiver = new BroadcastReceiver() {
			@Override
			public void onReceive(Context context, Intent intent) {
				double level =  (double) intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
				double scale =  (double) intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
				currentBattery = (level/scale)*100.0;
			}
		};

		IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
		appContext.registerReceiver(batteryStatusReceiver, filter);
		appContext.registerReceiver(batteryPercentReceiver, filter);

	}
	
	/*
	 * Decide where to execute the algorithm, locally (no offloading) or on the server (offloading is done).
	 * Returns true if the decision is to offload, false otherwise.
	 */
	private boolean decide(AlgName algName, String... parameters) {
		
		//The next 5 variables are used and updated only with the purpose of displaying information (some of them are redundant)
		androidEstimatedRuntime = -1;
		offloadingEstimatedTime = -1;
		serverEstimatedRuntime = -1;
		androidEstimatedNeededEnergy = -1;
		offloadingEstimatedNeededEnergy = -1;
		
		getConnectionDetails(); //Refresh connection info. If connection is available, does not check if server is available (it would take time).
		
		if (connAvailable && serverAvailable) {
			
			double cost = Algorithms.getCost(algName, parameters);
			double estServerTime = cost / ((double) SERVER_INST_MS); //Estimated server execution time
			double estAndroidTime = estServerTime * relationServerAndroid; //Estimated Android execution time
			double pingTime = ping;
			double estBattery2RunHere = cost * batteryPerInst; //Estimated battery needed to execute locally
			double estBattery2Offload;
			if (connWiFi) estBattery2Offload = batteryWiFi;
			else estBattery2Offload = battery3G; //Remember that if the connection type is other than Wi-Fi, we assume 3G
			
			androidEstimatedRuntime = estAndroidTime;
			offloadingEstimatedTime = pingTime + estServerTime;
			serverEstimatedRuntime = estServerTime;
			androidEstimatedNeededEnergy = estBattery2RunHere;
			offloadingEstimatedNeededEnergy = estBattery2Offload;
			
			if (pluggedDevice || connWiFi) { //We only take into account the time saving criteria
				if (pingTime + estServerTime < estAndroidTime) return true;
				else return false;
			}
			else { //We take into account both the time saving criteria and the battery saving criteria
				boolean timeSaving = false; //True if it supposes time saving doing the offloading
				boolean batterySaving = false; //True if it supposes battery saving doing the offloading
				if (pingTime + estServerTime < estAndroidTime) timeSaving = true;
				if (estBattery2Offload < estBattery2RunHere) batterySaving = true;
				if (timeSaving && batterySaving) return true; //Both criteria indicate to OFFLOAD
				else if (!timeSaving && !batterySaving) return false; //Both criteria indicate to NOT OFFLOAD
				else { //Threshold where each criteria indicates a different decision
					//We give more weight to the time saving criteria if the battery is full, and more to the battery saving criteria if it is empty.
					//Note this is a simple approach, ideally the user should be able to choose these weights depending on his/her preferences.
					double w1 = currentBattery;
					double w2 = 100.0 - w1;
					double timeGain;
					double batteryGain;
					if (timeSaving) { //Implies !batterySaving
						timeGain = estAndroidTime / (pingTime + estServerTime);
						batteryGain = estBattery2Offload / estBattery2RunHere;
					}
					else { //Implies !timeSaving && batterySaving
						timeGain = (pingTime + estServerTime) / estAndroidTime;
						batteryGain = estBattery2RunHere / estBattery2Offload;
					}
					if (timeGain * w1 > batteryGain * w2) return timeSaving;
					else return batterySaving;
				}
			}
			
		}
		else return false;
	}

	/* 
	 * Decide where to execute the algorithm, locally (no offloading) or on the server (offloading is done), executes it and retrieves the result.
	 * Returns the result on success (in String form), or the String "Error" on failure.
	 */
	public String execute(AlgName algName, String... parameters) {
		realServerTime = "None";
		overallTime = -1;
		doOffloading = decide(algName, parameters);
		long startTime = System.currentTimeMillis();
		String algResult = "";
		if (doOffloading) { //Do offloading
			String urlParams = Engine.urlEncodeParams(parameters);
			String queryString = Engine.SERVER_URL + "?algName=" + algName.toString() + urlParams;
			GetServerData getServerData = new GetServerData();
			getServerData.execute(queryString);
			try {
				algResult = getServerData.get(); //This can also return the word "Error"
			} catch (Exception e) {
				e.printStackTrace();
				serverAvailable = false;
				algResult = "Error";
			}
			if (algResult.equals("Error")) { //Unable to retrieve the data. URL may be invalid or the server may be down.
				serverAvailable = false;
			}
			else {
				try {
					realServerTime = getElementValueFromXML(algResult, "runtime");
					algResult = getElementValueFromXML(algResult, "result");
				} catch (Exception e) {
					e.printStackTrace();
					algResult = "Error";
				}
			}
		}
		else { //Do not offload, execute locally in the Android mobile device
			algResult = Algorithms.executeLocally(algName, parameters);
		}
		overallTime = System.currentTimeMillis() - startTime;
		return algResult;
	}

	private void loadEngine() {
		SharedPreferences sPrefs = appContext.getSharedPreferences(Engine.PREFS_NAME, Context.MODE_PRIVATE);
		relationServerAndroid = sPrefs.getFloat("deviceServerRelation", -1);
		batteryPerInst = sPrefs.getFloat("batteryPerInst", -1);
		battery3G = sPrefs.getFloat("battery3G", -1);
		batteryWiFi = sPrefs.getFloat("batteryWiFi", -1);
		if (relationServerAndroid == -1) { //It did not exist
			SharedPreferences.Editor editor = sPrefs.edit();
			relationServerAndroid = calculateRelation();
			editor.putFloat("deviceServerRelation", relationServerAndroid);
			calculateBatteryConsumption(); //Obtain all the relevant values for battery
			editor.putFloat("batteryPerInst", batteryPerInst);
			editor.putFloat("battery3G", battery3G);
			editor.putFloat("batteryWiFi", batteryWiFi);
			editor.commit();
		}
	}

	// Execute an algorithm in the Android device with a known cost in the server, in order to establish a speed relation (how many times faster is the server)
	private float calculateRelation() {
		long[] timesArray = new long[50];
		long startAlgorithmTime = System.currentTimeMillis();
		long endAlgorithmTime;
		long nLoops = 1000000;
		for (int i = 0; i < 50; i++) {
			Algorithms.executeLocally(AlgName.doSomeLoops, Long.valueOf(nLoops).toString());
			endAlgorithmTime = System.currentTimeMillis();
			timesArray[i] = endAlgorithmTime - startAlgorithmTime;
			startAlgorithmTime = endAlgorithmTime;
		}
		long timesAverage = Engine.calcAverage(timesArray);
		long AndroidInstMs = Algorithms.getCost(AlgName.doSomeLoops, Long.valueOf(nLoops).toString())/timesAverage;
		double cores = Engine.getNumCores(); //TODO Provisional fix for the real Samsung Galaxy Nexus device
		return (float) ((((double) Engine.SERVER_INST_MS) / ((double) AndroidInstMs)) / cores);
	}

	private void calculateBatteryConsumption() {
		// TODO These are fixed greedy values, instead, should be obtained from the battery details of the Android mobile device
		batteryPerInst = 1;
		battery3G = 60000000;
		batteryWiFi = 15000000;
	}

	private void averagePing() {
		pingCounter = 0;
		pingsArray = new long[10];
		timePingStart = System.currentTimeMillis();
		new GetPing().execute(Engine.SERVER_URL);
	}

	private class GetPing extends AsyncTask<String, Void, Integer> {

		@Override
		protected Integer doInBackground(String... urlAddress) {
			try {
				DefaultHttpClient httpClient = new DefaultHttpClient();
				HttpGet httpGet = new HttpGet(urlAddress[0]);
				ResponseHandler<String> resHandler = new BasicResponseHandler();
				httpClient.execute(httpGet, resHandler);
				return 0;
			} catch (Exception e) {
				e.printStackTrace();
				return -1;
			}
		}

		@Override
		protected void onPostExecute(Integer respCode) {
			if (respCode == -1) { //Ping failed
				serverAvailable = false;
			}
			else {
				long timePingEnd = System.currentTimeMillis();
				pingsArray[pingCounter] = timePingEnd - timePingStart;
				pingCounter++;
				if (pingCounter < 10) {
					timePingStart = System.currentTimeMillis();
					new GetPing().execute(Engine.SERVER_URL);
				}
				else if (pingCounter == 10) {
					ping = Engine.calcAverage(pingsArray);
				}
			}
		}	
	}

	private class GetServerData extends AsyncTask<String, Void, String> {
		@Override
		protected String doInBackground(String... urlAddress) {
			try {
				DefaultHttpClient httpClient = new DefaultHttpClient();
				HttpGet httpGet = new HttpGet(urlAddress[0]);
				ResponseHandler<String> resHandler = new BasicResponseHandler();
				String responseData = httpClient.execute(httpGet, resHandler);
				return responseData;
			} catch (Exception e) {
				e.printStackTrace();
				return "Error";
			}
		}
	}
	
	private static int getNumCores() {
	    //Private Class to display only CPU devices in the directory listing
	    class CpuFilter implements FileFilter {
	        @Override
	        public boolean accept(File pathname) {
	            //Check if filename is "cpu", followed by a single digit number
	            if(Pattern.matches("cpu[0-9]", pathname.getName())) {
	                return true;
	            }
	            return false;
	        }      
	    }

	    try {
	        //Get directory containing CPU info
	        File dir = new File("/sys/devices/system/cpu/");
	        //Filter to only list the devices we care about
	        File[] files = dir.listFiles(new CpuFilter());
	        //Return the number of cores (virtual CPU devices)
	        return files.length;
	    } catch(Exception e) {
	        //Default to return 1 core
	        return 1;
	    }
	}
	
	private static String urlEncodeParams(String... parameters) {
		if (parameters == null) return "";
		String urlParams = "";
		for (int i = 0; i < parameters.length; i++) {
			String urlEncodedParam;
			try {
				urlEncodedParam = URLEncoder.encode(parameters[i], "UTF-8");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
				return "Error";
			}
			urlParams = urlParams + "&param" + (i + 1) + "=" + urlEncodedParam;
		}
		return urlParams;
	}

	private static String getElementValueFromXML(String xmlString, String tagName) 
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = dbf.newDocumentBuilder();
		InputSource is = new InputSource();
		is.setCharacterStream(new StringReader(xmlString));
		Document doc = db.parse(is);
		NodeList summary = doc.getElementsByTagName(tagName);
		Element line = (Element) summary.item(0);
		return getCharacterDataFromElement(line);
	}

	private static String getCharacterDataFromElement(Element e) {
		Node child = e.getFirstChild();
		if (child instanceof CharacterData) {
			CharacterData cd = (CharacterData) child;
			return cd.getData();
		}
		return "";
	}

	private static long calcAverage (long[] valuesArray) {

		//Calculate the average
		long valuesSum = 0;
		for (int i = 0; i < valuesArray.length; i++) {
			valuesSum += valuesArray[i];
		}
		long average = valuesSum/valuesArray.length;

		//Recalculate the average omitting all values with a high deviation
		long niceValuesSum = 0;
		int niceValuesCount = 0;
		for (int i = 0; i < valuesArray.length; i++) {
			long auxValue = valuesArray[i];
			if (auxValue < 0) auxValue *= -1;
			long auxAverage = average;
			if (auxAverage < 0) auxAverage *= -1;
			if (auxValue <= auxAverage * 2) {
				niceValuesSum += valuesArray[i];
				niceValuesCount++;
			}
		}
		return niceValuesSum/niceValuesCount;
	}

}
